﻿using System.Collections.Concurrent;
using System.Reflection;

namespace Dyao.Book.Contract
{
    /// <summary>
    /// 属性变化记录
    /// </summary>
    public class PropertyChangeStore
    {
        private static ConcurrentDictionary<Type, IReadOnlyList<PropertyInfo>> _typeProperties = new ConcurrentDictionary<Type, IReadOnlyList<PropertyInfo>>();
        private readonly object _target;
        private IReadOnlyList<PropertyInfo> _properties;
        private DirtyCollection _dirtys;

        public PropertyChangeStore(object target)
        {
            if (target == null) throw new ArgumentNullException(nameof(target));

            this._target = target;

            _properties = _typeProperties.GetOrAdd(_target.GetType(), t => t.GetProperties());
        }

        protected DirtyCollection Dirtys
        {
            get
            {
                if (_dirtys == null)
                {
                    _dirtys = new DirtyCollection();
                }

                return _dirtys;
            }
        }

        protected virtual object this[string propertyName]
        {
            get
            {
                var p = GetProperty(propertyName);
                if (p.CanRead)
                {
                    return p.GetValue(_target, null);
                }

                throw new ArgumentException($"{propertyName}属性无法读取.");
            }
            set
            {
                var p = GetProperty(propertyName);
                if (p.CanWrite)
                {
                    p.SetValue(_target, value);
                    return;
                }

                //throw new ArgumentException($"{propertyName}属性无法写入.");
            }
        }

        /// <summary>
        /// 是否有属性被修改
        /// </summary>
        /// <returns></returns>
        public bool HasChanges() => _dirtys != null && _dirtys.Any();

        /// <summary>
        /// 属性<paramref name="propertyName"/>是否被修改
        /// </summary>
        /// <param name="propertyName">属性名称</param>
        /// <returns></returns>

        public bool HasChange(string propertyName) => _dirtys != null && _dirtys.Contains(propertyName);

        public IEnumerable<string> GetChanges()
        {
            if (_dirtys != null)
            {
                return _dirtys.GetAll();
            }

            return Enumerable.Empty<string>();
        }

        /// <summary>
        /// 如果<paramref name="value"/>与属性值不等，则设置值
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        protected virtual void SetValueIfNoEqual(string propertyName, object value)
        {
            if (OnPropertyChanging(propertyName, value))
            {
                this[propertyName] = value;
            }
        }

        private PropertyInfo GetProperty(string propertyName)
        {
            var p = _properties.FirstOrDefault(a => a.Name == propertyName);
            if (p == null)
            {
                throw new ArgumentException($"属性[{propertyName}]未找到");
            }

            return p;
        }

        protected virtual bool OnPropertyChanging(string propertyName, object newValue)
        {
            // 如果数据没有改变，不应该影响脏数据
            if (CheckEqual(this[propertyName], newValue)) return false;

            Dirtys[propertyName] = true;
            return true;
        }

        protected bool CheckEqual(object v1, object v2)
        {
            if (v1 == null && v2 == null) return true;
            if (v1 == null || v2 == null) return false;

            switch (Type.GetTypeCode(v1.GetType()))
            {
                case TypeCode.DateTime:
                    {
                        var d1 = (DateTime)v1;
                        var d2 = (DateTime)v2;

                        return d1.Ticks == d2.Ticks;
                    }
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                    return Convert.ToInt64(v1) == Convert.ToInt64(v2);
                case TypeCode.String:
                    return v1 + "" == v2 + "";
                default:
                    break;
            }

            return Equals(v1, v2);
        }

    }
}
